/**
 * 
 */
package dormouse.vfs.metadata;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.Stack;

import javax.imageio.IIOException;
import javax.imageio.stream.IIOByteBuffer;
import javax.imageio.stream.ImageInputStream;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;

/**
 * @author mfortner
 * 
 */
public class FileObjectImageInputStream implements ImageInputStream {

	private FileObject file = null;
	private InputStream stream = null;
	
    private Stack markByteStack = new Stack();

    private Stack markBitStack = new Stack();

    private boolean isClosed = false;


	// Length of the buffer used for readFully(type[], int, int)
	private static final int BYTE_BUF_LENGTH = 8192;

	/**
	 * Byte buffer used for readFully(type[], int, int). Note that this array is
	 * also used for bulk reads in readShort(), readInt(), etc, so it should be
	 * large enough to hold a primitive value (i.e. >= 8 bytes). Also note that
	 * this array is package protected, so that it can be used by
	 * ImageOutputStreamImpl in a similar manner.
	 */
	byte[] byteBuf = new byte[BYTE_BUF_LENGTH];

	/**
	 * The byte order of the stream as an instance of the enumeration class
	 * <code>java.nio.ByteOrder</code>, where <code>ByteOrder.BIG_ENDIAN</code>
	 * indicates network byte order and <code>ByteOrder.LITTLE_ENDIAN</code>
	 * indicates the reverse order. By default, the value is
	 * <code>ByteOrder.BIG_ENDIAN</code>.
	 */
	protected ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;

	/**
	 * The current read position within the stream. Subclasses are responsible
	 * for keeping this value current from any method they override that alters
	 * the read position.
	 */
	protected long streamPos;

	/**
	 * The current bit offset within the stream. Subclasses are responsible for
	 * keeping this value current from any method they override that alters the
	 * bit offset.
	 */
	protected int bitOffset;

	/**
	 * The position prior to which data may be discarded. Seeking to a smaller
	 * position is not allowed. <code>flushedPos</code> will always be >= 0.
	 */
	protected long flushedPos = 0;

	/**
	 * Constructor
	 * 
	 * @param file
	 */
	public FileObjectImageInputStream(FileObject file) {
		this.file = file;
		try {
			this.stream = this.file.getContent().getInputStream();
		} catch (FileSystemException e) {
			e.printStackTrace();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.imageio.stream.ImageInputStreamImpl#read()
	 */
	@Override
	public int read() throws IOException {
        checkClosed();
        bitOffset = 0;
        int val = this.stream.read();
        if (val != -1) {
            ++streamPos;
        }
        return val;
		
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.imageio.stream.ImageInputStreamImpl#read(byte[], int, int)
	 */
	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		checkClosed();
		int nbytes =  this.stream.read(b, off, len);
        if (nbytes != -1) {
            streamPos += nbytes;
        }
        return nbytes;
	}

	@Override
	public void close() throws IOException {
		this.stream.close();
	}

	@Override
	public void flush() throws IOException {
		flushBefore(getStreamPosition());
	}

	@Override
	public void flushBefore(long pos) throws IOException {
        checkClosed();
        if (pos < flushedPos) {
            throw new IndexOutOfBoundsException("pos < flushedPos!");
        }
        if (pos > getStreamPosition()) {
            throw new IndexOutOfBoundsException("pos > getStreamPosition()!");
        }
        // Invariant: flushedPos >= 0
        flushedPos = pos;

	}

	@Override
	public int getBitOffset() throws IOException {
		checkClosed();
		return this.bitOffset;
	}

	@Override
	public ByteOrder getByteOrder() {
		return this.byteOrder;
	}

	@Override
	public long getFlushedPosition() {
		return this.flushedPos;
	}

	@Override
	public long getStreamPosition() throws IOException {
		checkClosed();
		return this.streamPos;
	}

	@Override
	public boolean isCached() {
		return false;
	}

	@Override
	public boolean isCachedFile() {
		return false;
	}

	@Override
	public boolean isCachedMemory() {
		return false;
	}

	@Override
	public long length() throws IOException {
		return this.file.getContent().getSize();
	}

	@Override
	public void mark() {
        try {
            markByteStack.push(new Long(getStreamPosition()));
            markBitStack.push(new Integer(getBitOffset()));
        } catch (IOException e) {
        }
	}

	@Override
	public int read(byte[] b) throws IOException {
		return read(b, 0, b.length);
	}

	@Override
	public int readBit() throws IOException {
        checkClosed();

        // Compute final bit offset before we call read() and seek()
        int newBitOffset = (this.bitOffset + 1) & 0x7;

        int val = read();
        if (val == -1) {
            throw new EOFException();
        }

        if (newBitOffset != 0) {
            // Move byte position back if in the middle of a byte
            seek(getStreamPosition() - 1);
            // Shift the bit to be read to the rightmost position
            val >>= 8 - newBitOffset;
        }
        this.bitOffset = newBitOffset;

        return val & 0x1;
	}

	@Override
	public long readBits(int numBits) throws IOException {
        checkClosed();

        if (numBits < 0 || numBits > 64) {
            throw new IllegalArgumentException();
        }
        if (numBits == 0) {
            return 0L;
        }

        // Have to read additional bits on the left equal to the bit offset
        int bitsToRead = numBits + bitOffset;

        // Compute final bit offset before we call read() and seek()
        int newBitOffset = (this.bitOffset + numBits) & 0x7;
        
        // Read a byte at a time, accumulate
        long accum = 0L;
        while (bitsToRead > 0) {
            int val = read();
            if (val == -1) {
                throw new EOFException();
            }

            accum <<= 8;
            accum |= val;
            bitsToRead -= 8;
        }

        // Move byte position back if in the middle of a byte
        if (newBitOffset != 0) {
            seek(getStreamPosition() - 1);
        }
        this.bitOffset = newBitOffset;

        // Shift away unwanted bits on the right.
        accum >>>= (-bitsToRead); // Negative of bitsToRead == extra bits read
        
        // Mask out unwanted bits on the left
        accum &= (-1L >>> (64 - numBits));

        return accum;	}

	@Override
	public boolean readBoolean() throws IOException {
		int ch = this.read();
		if (ch < 0) {
			throw new EOFException();
		}
		return (ch != 0);
	}

	@Override
	public byte readByte() throws IOException {
		int ch = this.read();
		if (ch < 0) {
			throw new EOFException();
		}
		return (byte) ch;
	}

	@Override
	public void readBytes(IIOByteBuffer buf, int len) throws IOException {
		if (len < 0) {
			throw new IndexOutOfBoundsException("len < 0!");
		}
		if (buf == null) {
			throw new NullPointerException("buf == null!");
		}

		byte[] data = new byte[len];
		len = read(data, 0, len);

		buf.setData(data);
		buf.setOffset(0);
		buf.setLength(len);

	}

	@Override
	public char readChar() throws IOException {
		return (char) readShort();
	}

	@Override
	public double readDouble() throws IOException {
		return Double.longBitsToDouble(readLong());
	}

	@Override
	public float readFloat() throws IOException {
		return Float.intBitsToFloat(readInt());
	}

	@Override
	public void readFully(byte[] b) throws IOException {
		readFully(b, 0, b.length);

	}

	@Override
	public void readFully(byte[] b, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > b.length!");
		}

		while (len > 0) {
			int nbytes = read(b, off, len);
			if (nbytes == -1) {
				throw new EOFException();
			}
			off += nbytes;
			len -= nbytes;
		}
	}

	@Override
	public void readFully(short[] s, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > s.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > s.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 2);
			readFully(byteBuf, 0, nelts * 2);
			toShorts(byteBuf, s, off, nelts);
			off += nelts;
			len -= nelts;
		}

	}

	@Override
	public void readFully(char[] c, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > c.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > c.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 2);
			readFully(byteBuf, 0, nelts * 2);
			toChars(byteBuf, c, off, nelts);
			off += nelts;
			len -= nelts;
		}

	}

	@Override
	public void readFully(int[] i, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > i.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > i.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 4);
			readFully(byteBuf, 0, nelts * 4);
			toInts(byteBuf, i, off, nelts);
			off += nelts;
			len -= nelts;
		}
	}

	@Override
	public void readFully(long[] l, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > l.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > l.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 8);
			readFully(byteBuf, 0, nelts * 8);
			toLongs(byteBuf, l, off, nelts);
			off += nelts;
			len -= nelts;
		}

	}

	@Override
	public void readFully(float[] f, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > f.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > f.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 4);
			readFully(byteBuf, 0, nelts * 4);
			toFloats(byteBuf, f, off, nelts);
			off += nelts;
			len -= nelts;
		}
	}

	@Override
	public void readFully(double[] d, int off, int len) throws IOException {
		// Fix 4430357 - if off + len < 0, overflow occurred
		if (off < 0 || len < 0 || off + len > d.length || off + len < 0) {
			throw new IndexOutOfBoundsException(
					"off < 0 || len < 0 || off + len > d.length!");
		}

		while (len > 0) {
			int nelts = Math.min(len, byteBuf.length / 8);
			readFully(byteBuf, 0, nelts * 8);
			toDoubles(byteBuf, d, off, nelts);
			off += nelts;
			len -= nelts;
		}
	}

	@Override
	public int readInt() throws IOException {
		if (read(byteBuf, 0, 4) < 0) {
			throw new EOFException();
		}

		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			return (((byteBuf[0] & 0xff) << 24) | ((byteBuf[1] & 0xff) << 16)
					| ((byteBuf[2] & 0xff) << 8) | ((byteBuf[3] & 0xff) << 0));
		} else {
			return (((byteBuf[3] & 0xff) << 24) | ((byteBuf[2] & 0xff) << 16)
					| ((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0));
		}
	}

	@Override
	public String readLine() throws IOException {
		StringBuffer input = new StringBuffer();
		int c = -1;
		boolean eol = false;

		while (!eol) {
			switch (c = read()) {
			case -1:
			case '\n':
				eol = true;
				break;
			case '\r':
				eol = true;
				long cur = getStreamPosition();
				if ((read()) != '\n') {
					seek(cur);
				}
				break;
			default:
				input.append((char) c);
				break;
			}
		}
		if ((c == -1) && (input.length() == 0)) {
			return null;
		}
		return input.toString();
	}

	@Override
	public long readLong() throws IOException {
		// REMIND: Once 6277756 is fixed, we should do a bulk read of all 8
		// bytes here as we do in readShort() and readInt() for even better
		// performance (see 6347575 for details).
		int i1 = readInt();
		int i2 = readInt();

		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			return ((long) i1 << 32) + (i2 & 0xFFFFFFFFL);
		} else {
			return ((long) i2 << 32) + (i1 & 0xFFFFFFFFL);
		}
	}

	@Override
	public short readShort() throws IOException {
		if (read(byteBuf, 0, 2) < 0) {
			throw new EOFException();
		}

		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			return (short) (((byteBuf[0] & 0xff) << 8) | ((byteBuf[1] & 0xff) << 0));
		} else {
			return (short) (((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0));
		}
	}

	@Override
	public String readUTF() throws IOException {
		this.bitOffset = 0;

		// Fix 4494369: method ImageInputStreamImpl.readUTF()
		// does not work as specified (it should always assume
		// network byte order).
		ByteOrder oldByteOrder = getByteOrder();
		setByteOrder(ByteOrder.BIG_ENDIAN);

		String ret;
		try {
			ret = DataInputStream.readUTF(this);
		} catch (IOException e) {
			// Restore the old byte order even if an exception occurs
			setByteOrder(oldByteOrder);
			throw e;
		}

		setByteOrder(oldByteOrder);
		return ret;
	}

	@Override
	public int readUnsignedByte() throws IOException {
		int ch = this.read();
		if (ch < 0) {
			throw new EOFException();
		}
		return ch;
	}

	@Override
	public long readUnsignedInt() throws IOException {
		return ((long) readInt()) & 0xffffffffL;
	}

	@Override
	public int readUnsignedShort() throws IOException {
		return ((int) readShort()) & 0xffff;
	}

	@Override
	public void reset() throws IOException {
        if (markByteStack.empty()) {
            return;
        }

        long pos = ((Long)markByteStack.pop()).longValue();
        if (pos < flushedPos) {
            throw new IIOException
                ("Previous marked position has been discarded!");
        }
        seek(pos);

        int offset = ((Integer)markBitStack.pop()).intValue();
        setBitOffset(offset);
	}

	@Override
	public void seek(long pos) throws IOException {
        checkClosed();

        // This test also covers pos < 0
        if (pos < flushedPos) {
            throw new IndexOutOfBoundsException("pos < flushedPos!");
        }

        this.streamPos = pos;
        this.bitOffset = 0;
	}

	private void checkClosed() throws IOException{
		 if (isClosed) {
	            throw new IOException("closed");
	        }
		
	}

	@Override
	public void setBitOffset(int bitOffset) throws IOException {
		this.bitOffset = bitOffset;
	}

	@Override
	public void setByteOrder(ByteOrder byteOrder) {
		this.byteOrder = byteOrder;
	}

	@Override
	public int skipBytes(int n) throws IOException {
		return (int) this.stream.skip(n);
	}

	@Override
	public long skipBytes(long n) throws IOException {
		return this.stream.skip(n);
	}

	private void toShorts(byte[] b, short[] s, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				s[off + j] = (short) ((b0 << 8) | b1);
				boff += 2;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 1];
				int b1 = b[boff] & 0xff;
				s[off + j] = (short) ((b0 << 8) | b1);
				boff += 2;
			}
		}
	}

	private void toChars(byte[] b, char[] c, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				c[off + j] = (char) ((b0 << 8) | b1);
				boff += 2;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 1];
				int b1 = b[boff] & 0xff;
				c[off + j] = (char) ((b0 << 8) | b1);
				boff += 2;
			}
		}
	}

	private void toInts(byte[] b, int[] i, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				int b2 = b[boff + 2] & 0xff;
				int b3 = b[boff + 3] & 0xff;
				i[off + j] = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				boff += 4;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 3];
				int b1 = b[boff + 2] & 0xff;
				int b2 = b[boff + 1] & 0xff;
				int b3 = b[boff] & 0xff;
				i[off + j] = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				boff += 4;
			}
		}
	}

	private void toLongs(byte[] b, long[] l, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				int b2 = b[boff + 2] & 0xff;
				int b3 = b[boff + 3] & 0xff;
				int b4 = b[boff + 4];
				int b5 = b[boff + 5] & 0xff;
				int b6 = b[boff + 6] & 0xff;
				int b7 = b[boff + 7] & 0xff;

				int i0 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				int i1 = (b4 << 24) | (b5 << 16) | (b6 << 8) | b7;

				l[off + j] = ((long) i0 << 32) | (i1 & 0xffffffffL);
				boff += 8;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 7];
				int b1 = b[boff + 6] & 0xff;
				int b2 = b[boff + 5] & 0xff;
				int b3 = b[boff + 4] & 0xff;
				int b4 = b[boff + 3];
				int b5 = b[boff + 2] & 0xff;
				int b6 = b[boff + 1] & 0xff;
				int b7 = b[boff] & 0xff;

				int i0 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				int i1 = (b4 << 24) | (b5 << 16) | (b6 << 8) | b7;

				l[off + j] = ((long) i0 << 32) | (i1 & 0xffffffffL);
				boff += 8;
			}
		}
	}

	private void toFloats(byte[] b, float[] f, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				int b2 = b[boff + 2] & 0xff;
				int b3 = b[boff + 3] & 0xff;
				int i = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				f[off + j] = Float.intBitsToFloat(i);
				boff += 4;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 3];
				int b1 = b[boff + 2] & 0xff;
				int b2 = b[boff + 1] & 0xff;
				int b3 = b[boff + 0] & 0xff;
				int i = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				f[off + j] = Float.intBitsToFloat(i);
				boff += 4;
			}
		}
	}

	private void toDoubles(byte[] b, double[] d, int off, int len) {
		int boff = 0;
		if (byteOrder == ByteOrder.BIG_ENDIAN) {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff];
				int b1 = b[boff + 1] & 0xff;
				int b2 = b[boff + 2] & 0xff;
				int b3 = b[boff + 3] & 0xff;
				int b4 = b[boff + 4];
				int b5 = b[boff + 5] & 0xff;
				int b6 = b[boff + 6] & 0xff;
				int b7 = b[boff + 7] & 0xff;

				int i0 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				int i1 = (b4 << 24) | (b5 << 16) | (b6 << 8) | b7;
				long l = ((long) i0 << 32) | (i1 & 0xffffffffL);

				d[off + j] = Double.longBitsToDouble(l);
				boff += 8;
			}
		} else {
			for (int j = 0; j < len; j++) {
				int b0 = b[boff + 7];
				int b1 = b[boff + 6] & 0xff;
				int b2 = b[boff + 5] & 0xff;
				int b3 = b[boff + 4] & 0xff;
				int b4 = b[boff + 3];
				int b5 = b[boff + 2] & 0xff;
				int b6 = b[boff + 1] & 0xff;
				int b7 = b[boff] & 0xff;

				int i0 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
				int i1 = (b4 << 24) | (b5 << 16) | (b6 << 8) | b7;
				long l = ((long) i0 << 32) | (i1 & 0xffffffffL);

				d[off + j] = Double.longBitsToDouble(l);
				boff += 8;
			}
		}
	}

}
